# Copyright (C) 2007 Richard Boulton
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
r"""callbacks.py: Callback implementation, which uses weakrefs for methods.

You can get a similar effect using PyDispatcher, but this implementation is a
lot simpler (while also being a lot more limited).

To use, first create a CallbackList, to hold a set of callbacks:
>>> l = CallbackList()

Next, add a callback function to the list:
>>> def cb1(arg): print "cb1:%s" % arg
>>> l.append(cb1)

And, append a callback method:
>>> class cbcls(object):
...     def cb2(self, arg): print "cb2:%s" % arg
>>> obj = cbcls()
>>> l.append(obj.cb2)

Finally, all the callbacks in the list can be called - any arguments supplied
will be passed to all the callbacks.
>>> l("hello")
cb1:hello
cb2:hello

Even if the callback function goes out of scope, the function callback will be
called:
>>> del cb1
>>> l("hello")
cb1:hello
cb2:hello

However, if the object for the callback method goes out of scope, the method
callback won't be called:

>>> del obj
>>> l("hello")
cb1:hello

"""

import weakref
import inspect

class CallbackList(object):
    def __init__(self):
        """Make a new, empty, callback list.

        """

        # If we just held the callbacks we were passed, method callbacks would
        # be discarded when the method object is deallocated (method objects
        # are usually transient objects, so this would usually be too soon).
        #
        # However, we actually want the callback to be discarded when the
        # object which the method is bound to is deallocated.  Therefore, we
        # split methods into two parts - the function, and the object which the
        # method is bound to, and keep a weakref to the bound object, instead
        # of the method itself.
        #
        # Thus, cbrefs holds two types of item: for functions, it holds a
        # weakref to the function - for methods, it holds a tuple: (function,
        # weakref to bound object).
        self.cbrefs = []

    def __call__(self, *args, **kwargs):
        """Call all the callbacks in the list.

        If any have become invalid, they will be dropped from the list.

        """
        old = []
        for pos in xrange(len(self.cbrefs)):
            cb = self.cbrefs[pos]
            if isinstance(cb, tuple):
                cbself = cb[1]()
                if cbself is None:
                    old.append(pos)
                else:
                    cb[0](cbself, *args, **kwargs)
            else:
                cb(*args, **kwargs)

        if len(old) != 0:
            old.reverse()
            for pos in old:
                del self.cbrefs[pos]

    def append(self, cb):
        """Append a callback to the list.

        If the callback is a method, a weakref will be used, and the callback
        will be discarded automatically when the object which the method is
        bound to is discarded; the callback will not keep the object alive.

        If the callback is already in the list, the earlier version of the
        callback will be removed (so, in effect, the existing callback will be
        moved to the end of the list).

        """
        self.remove(cb)
        if inspect.ismethod(cb):
            self.cbrefs.append((cb.im_func, weakref.ref(cb.im_self)))
        else:
            self.cbrefs.append(cb)

    def remove(self, cb):
        """Remove a callback from the list.

        Note that, if the callback is not found, this doesn't report an error
        indicating this.

        """
        ismethod = inspect.ismethod(cb)

        for pos in xrange(len(self.cbrefs)):
            oldcb = self.cbrefs[pos]
            if isinstance(oldcb, tuple):
                if ismethod:
                    oldcbself = oldcb[1]()
                    if oldcbself is not None:
                        if oldcbself is cb.im_self and oldcb[0] is cb.im_func:
                            del self.cbrefs[pos]
                            return
            else:
                if oldcb is cb:
                    del self.cbrefs[pos]
                    return


# Test cases
__test__ = {
'addremove': '''

Test adding and removing items from a callback list:

Set up a callback list, with a few functions and methods:

>>> def cb1(arg): print "cb1:%s" % arg
>>> def cb2(arg): print "cb2:%s" % arg
>>> class cbcls1(object):
...     def __init__(self, val): self.val = val
...     def cb(self, arg): print "cb%d:%s" % (self.val, arg)
>>> class cbcls2(object):
...     def __init__(self, val): self.val = val
...     def cb(self, arg): print "cb%d:%s" % (self.val, arg)


>>> l = CallbackList()
>>> l.append(cb1)
>>> l.append(cb2)
>>> obj3 = cbcls1(3)
>>> obj4 = cbcls1(4)
>>> l.append(obj3.cb)
>>> l.append(obj4.cb)


The initial call should call all the callbacks:
>>> l('hi')
cb1:hi
cb2:hi
cb3:hi
cb4:hi


Appending an existing callback changes the order:
>>> l.append(obj3.cb)
>>> l('hi')
cb1:hi
cb2:hi
cb4:hi
cb3:hi

>>> l.append(cb2)
>>> l('hi')
cb1:hi
cb4:hi
cb3:hi
cb2:hi

Methods can be explicitly removed from the callback list (even though the
actual method object may be different, a method callback with the same bound
object and the same function will be removed):
>>> l.remove(obj3.cb)
>>> l('hi')
cb1:hi
cb4:hi
cb2:hi

Removing something which wasn't there in the first place doesn't cause any
problems (or any change)
>>> l.remove(obj3.cb)
>>> l('hi')
cb1:hi
cb4:hi
cb2:hi

We can put a callback back (but it goes to the end of the list)
>>> l.append(obj3.cb)
>>> l('hi')
cb1:hi
cb4:hi
cb2:hi
cb3:hi

If the objects go out of scope, the corresponding method callbacks are no
longer called:
>>> del obj3
>>> del obj4
>>> l('hi')
cb1:hi
cb2:hi

However, if the callback functions go out of scope, the callbacks are still
called:
>>> del cb2
>>> l('hi')
cb1:hi
cb2:hi

But callback functions can be explicitly removed:
>>> l.remove(cb1)
>>> l('hi')
cb2:hi

'''
}

if __name__ == '__main__':
    import doctest, sys
    doctest.testmod(sys.modules[__name__])
